package com.yiban.demo.Station.Service.ServiceImp;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yiban.demo.Order.Entity.OrderTransfer;
import com.yiban.demo.Order.OrderEvent.DeliveryOrderEvent;
import com.yiban.demo.Order.OrderEvent.EndOrderEvent;
import com.yiban.demo.Order.OrderEvent.NewOrderEvent;
import com.yiban.demo.Station.Controller.PushStationServiceHandle;
import com.yiban.demo.Station.DAO.StationMapper;
import com.yiban.demo.Station.Entity.NearByStation;
import com.yiban.demo.Station.Entity.Station;
import com.yiban.demo.Station.Entity.StationOrderMessage;
import com.yiban.demo.Station.Entity.StationStatus;
import com.yiban.demo.Station.Service.StationService;
import com.yiban.demo.Util.JsonUtil;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.BoundGeoOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.PreDestroy;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
public class StationServiceImp implements StationService {
    private static String STATION_POS_PREFIX = "station:pos";
    //stationId:station
    private ConcurrentHashMap<Integer,Station> stations;
    private StationMapper stationMapper;
    //key->string->STATION_POS_PREFIX
    //value->integer->stationId
    private RedisTemplate<String, Integer> redisTemplate;
    private Double maxDistance;
    private PushStationServiceHandle pushStationServiceHandle;
    @Autowired
    public StationServiceImp(StationMapper stationMapper, RedisTemplate redisTemplate, @Value("${station.max-transfer-distance}") Double maxDistance, PushStationServiceHandle pushStationServiceHandle) {
        this.stationMapper = stationMapper;

        stations = stationMapper.selectList(new LambdaUpdateWrapper<Station>().eq(Station::isPass,true))
                .stream()
                .collect(Collectors.toMap(Station::getStationId, Function.identity(),(s1,s2)->s1,ConcurrentHashMap::new));
        this.redisTemplate = redisTemplate;
        this.maxDistance = maxDistance;
        this.pushStationServiceHandle = pushStationServiceHandle;

        //防止上次异常退出没有清空数据导致数据污染
        clearPosInfo();

        BoundGeoOperations boundHashOperations = redisTemplate.boundGeoOps(STATION_POS_PREFIX);

        Map<Integer, Point> poses = stations.values().stream()
                .collect(Collectors.toMap(Station::getStationId, (s) -> new Point(s.getLongitudes().doubleValue(), s.getLatitudes().doubleValue())));
        //防止因为没有获取到数据后，add中非empty断言导致启动失败
        //你不信的化可以注释掉这个if，并在不修改其余设置，且在station数据库为空的情况下，启动项目试试
        if (!poses.isEmpty()) {
            boundHashOperations.add(poses);
        }

    }

    @PreDestroy
    public void clearPosInfo(){
        redisTemplate.delete(STATION_POS_PREFIX);
    }


    @Override
    public boolean registerStation(Station station,Integer userId) {
        station.setPass(false);
        station.setUserId(userId);
        return stationMapper.insert(station) == 1;
    }

    @Override
    public boolean passRegisterRequest(Integer stationId) {
        if (stationMapper.updateIsPass(true, stationId) != 1) {
            return false;
        }
        Station station = stationMapper.selectById(stationId);
        stations.put(station.getUserId(),station);
        redisTemplate.opsForGeo().add(STATION_POS_PREFIX, new Point(station.getLongitudes().doubleValue(),station.getLatitudes().doubleValue()), station.getUserId());
        return true;
    }

    @Override
    public List<Station> getRegisterRequest(Long current, Long size) {
        return stationMapper.selectPage(new Page<>(current,size),new LambdaQueryWrapper<Station>().eq(Station::isPass,false)).getRecords();
    }

    @Override
    public Map<Integer, Station> getPushStations() {
        return stations;
    }

    @Override
    public Optional<List<NearByStation>> getNearStation(BigDecimal longitudes, BigDecimal latitudes) {
        Point userPos = new Point(longitudes.doubleValue(), latitudes.doubleValue());
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeDistance()
                .includeCoordinates()
                .sortAscending()
                .limit(3);
        GeoResults<RedisGeoCommands.GeoLocation<Integer>> radius = redisTemplate.opsForGeo().radius(STATION_POS_PREFIX, new Circle(userPos, maxDistance), args);

        return Optional.ofNullable(radius)
                .map(GeoResults::getContent)
                .map(c ->
                            c.stream()
                            .map(res -> {
                                var content = res.getContent();
                                return new NearByStation(stations.get(content.getName()), content.getPoint(), res.getDistance()); })
                            .filter(nearByStation -> isOnline(nearByStation.getStationId()))
                            .collect(Collectors.toList())
                );
    }

    @Deprecated
    //todo 还没想好
    public void removeStation(Integer stationId){
        stations.remove(stationId);
        redisTemplate.boundGeoOps(STATION_POS_PREFIX).remove(stationId);
    }

    @Override
    //不在线或者不存在都返回false
    public boolean isOnline(Integer stationId) {
        return Optional.ofNullable(stations.get(stationId))
                .map(Station::getUserId)
                .map(pushStationServiceHandle::isOnline)
                .orElse(false);
    }

    @Override
    public Optional<Station> getStation(Integer stationId) {
        return Optional.ofNullable(stations.get(stationId));
    }

    @Override
    @SneakyThrows
    public StationStatus sentMessage(Integer stationId, StationOrderMessage msg){
        Station station = stations.get(stationId);
        if (station == null) {
            return StationStatus.NOT_FOUND;
        }
        if (!isOnline(stationId)) {
            return StationStatus.NOT_ONLINE;
        }

        pushStationServiceHandle.sendMessage(JsonUtil.defaultJsonify(msg), station.getUserId());

        return StationStatus.NORMAL;
    }

    @Override
    public List<Station> getOnlineStations(){
        return stations.values()
                .stream()
                .filter(s -> isOnline(s.getStationId()))
                .collect(Collectors.toList());
    }

   @Async
   @EventListener(classes = DeliveryOrderEvent.class)
   @Override
   public void deliveryEventListen(DeliveryOrderEvent deliveryOrderEvent){
        sentMessage(deliveryOrderEvent.getPo().getStationId(), StationOrderMessage.generate(StationOrderMessage.StationOrderMessageClass.DELIVERY, OrderTransfer.transToOrderVo(deliveryOrderEvent.getPo())));
   }
   @Async
   @EventListener(classes = NewOrderEvent.class)
   @Override
   public void newOrderEventListen(NewOrderEvent newOrderEvent){
        sentMessage(newOrderEvent.getPo().getStationId(), StationOrderMessage.generate(StationOrderMessage.StationOrderMessageClass.ADD, OrderTransfer.transToOrderVo(newOrderEvent.getPo())));
   }

   @Async
   @EventListener(classes = EndOrderEvent.class)
   @Override
   public void endOrderEventListen(EndOrderEvent endOrderEvent){
        sentMessage(endOrderEvent.getPo().getStationId(), StationOrderMessage.generate(StationOrderMessage.StationOrderMessageClass.END, OrderTransfer.transToOrderVo(endOrderEvent.getPo())));
   }

}
